package com.jzo2o.market.service.impl;

import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.common.expcetions.ForbiddenOperationException;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.common.utils.*;
import com.jzo2o.market.constants.RedisConstants;
import com.jzo2o.market.constants.TabTypeConstants;
import com.jzo2o.market.enums.ActivityStatusEnum;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.mapper.ActivityMapper;
import com.jzo2o.market.mapper.CouponMapper;
import com.jzo2o.market.mapper.CouponWriteOffMapper;
import com.jzo2o.market.model.domain.Activity;
import com.jzo2o.market.model.domain.Coupon;
import com.jzo2o.market.model.domain.CouponWriteOff;
import com.jzo2o.market.model.dto.request.ActivityQueryForPageReqDTO;
import com.jzo2o.market.model.dto.request.ActivitySaveReqDTO;
import com.jzo2o.market.model.dto.response.ActivityInfoResDTO;
import com.jzo2o.market.model.dto.response.CountResDTO;
import com.jzo2o.market.model.dto.response.SeizeCouponInfoResDTO;
import com.jzo2o.market.service.IActivityService;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.market.service.ICouponWriteOffService;
import com.jzo2o.mysql.utils.PageUtils;
import groovy.transform.Undefined;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.jzo2o.market.constants.RedisConstants.RedisKey.*;
import static com.jzo2o.market.enums.ActivityStatusEnum.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author itcast
 * @since 2023-09-16
 */
@Service
public class ActivityServiceImpl extends ServiceImpl<ActivityMapper, Activity> implements IActivityService {
    private static final int MILLION = 1000000;


    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ICouponService couponService;
    @Autowired
    private ICouponWriteOffService couponWriteOffService;
    @Autowired
    private CouponMapper couponMapper;
    @Autowired
    private CouponWriteOffMapper couponWriteOffMapper;


    /**
     * 保存或更新优惠券活动
     *
     * @param dto
     */
    @Override
    public void saveOrUpdateActivity(ActivitySaveReqDTO dto) {
        //0.这里需要验证一些优惠的合法性
        dto.check();//这里直接写在了dto中进行验证了

        //因为传递过来的dto 中缺少库存和状态值，所以需要补全
        Activity activity = BeanUtils.copyBean(dto, Activity.class);
        activity.setStatus(ActivityStatusEnum.NO_DISTRIBUTE.getStatus());//待生效
        activity.setStockNum(dto.getTotalNum());//库存一开始等于发放数量

        //保存优惠券活动表
        this.saveOrUpdate(activity);
    }


    /**
     * 运营端分页查询活动
     *
     * @param dto 查询条件
     * @return 优惠券活动分页结果
     */
    @Override
    public PageResult<ActivityInfoResDTO> findByPage(ActivityQueryForPageReqDTO dto) {
        //根据分析请求参数和响应参数 其中这两个字段在activity表中不存在，所以需要去coupon优惠券表和coupon_write_off中进行查询
        //receiveNum：优惠券领取数量，此字段需要去优惠券表中根据当前活动的id进行查询
        //writeOffNum：优惠券核销数量，此字段需要去核销表中根据当前活动的id进行查询

        //分页查询
        //1.设置分页参数
        Page<Activity> page = PageUtils.parsePageQuery(dto, Activity.class);

        //2.设置查询条件
        LambdaQueryWrapper<Activity> wrapper = Wrappers.<Activity>lambdaQuery()
                .eq(dto.getId() != null, Activity::getId, dto.getId())
                .like(StringUtils.isNotBlank(dto.getName()), Activity::getName, dto.getName())
                .eq(dto.getType() != null, Activity::getType, dto.getType())
                .eq(dto.getStatus() != null, Activity::getStatus, dto.getStatus());

        //3.执行查询
        page = this.page(page, wrapper);
        if (CollUtils.isEmpty(page.getRecords())) {
            return new PageResult<>(0L, 0L, null);
        }
        //4.提前查询本页所有活动id对应的优惠券领取数量, 封装到一个Map<活动id,优惠券领取数量>集合中备用(就是给for循环中的多次调用数据库的语句进行一个优化)
        //4.1 获取本页所有活动id
        List<Long> activityIdList = page.getRecords().stream().map(Activity::getId).collect(Collectors.toList());

        //4.2 根据活动id集合查询对应的优惠劵领取数量 就是根据获取活动id集合去优惠劵表中查询对应的数据
        List<CountResDTO> couponList = couponMapper.selectCountByActivityIdList(activityIdList);//优惠券领取数量 key:活动id,value:优惠券领取数量
        List<CountResDTO> couponWriteOffList = couponWriteOffMapper.selectCountByActivityIdList(activityIdList);//优惠券核销数量 key:活动id,value:优惠券核销数量

        //4.3 将优惠劵领取数量集合转换成Map<活动id,优惠券领取数量>集合
        Map<Long, Integer> couponMap = couponList.stream().collect(Collectors.toMap(CountResDTO::getActivityId, CountResDTO::getNum));//优惠券领取数量
        Map<Long, Integer> couponWriteOffMap = couponWriteOffList.stream().collect(Collectors.toMap(CountResDTO::getActivityId, CountResDTO::getNum));//优惠券核销数量

        //5.封装数据返回结果()
        List<ActivityInfoResDTO> list = page.getRecords().stream().map(e -> {
            ActivityInfoResDTO activityInfoResDTO = BeanUtils.copyBean(e, ActivityInfoResDTO.class);

            //由于for循环里面存在多次调用数据库,这里就造成了性能的浪费了,所以这里要进行一个优化
            /*int count = couponService.count(Wrappers.<Coupon>lambdaQuery().eq(Coupon::getActivityId, e.getId()));
            int count1 = couponWriteOffService.count(Wrappers.<CouponWriteOff>lambdaQuery().eq(CouponWriteOff::getActivityId, e.getId()));
            activityInfoResDTO.setReceiveNum(count);//领取优惠券数量
            activityInfoResDTO.setWriteOffNum(count1);//核销优惠券数量*/

            //优化后的代码
            activityInfoResDTO.setReceiveNum(couponMap.getOrDefault(e.getId(), 0));//领取优惠券数量
            activityInfoResDTO.setWriteOffNum(couponWriteOffMap.getOrDefault(e.getId(), 0));//核销优惠券数量

            return activityInfoResDTO;
        }).collect(Collectors.toList());

        return new PageResult<>(page.getTotal(), page.getPages(), list);
    }

    /**
     * 根据id查询活动详情 ,页面回显
     *
     * @param id
     * @return
     */
    @Override
    public ActivityInfoResDTO findById(Long id) {
        //1.根据活动id查询activity表数据
        Activity activity = this.getById(id);
        ActivityInfoResDTO activityInfoResDTO = BeanUtils.copyProperties(activity, ActivityInfoResDTO.class);
        //2.根据活动id查询coupon中的领取数量
        activityInfoResDTO.setReceiveNum(couponService.count(Wrappers.<Coupon>lambdaQuery().eq(Coupon::getActivityId, id)));
        //3.根据活动id查询coupon_write_off表中的核销数量
        activityInfoResDTO.setWriteOffNum(couponWriteOffService.count(Wrappers.<CouponWriteOff>lambdaQuery().eq(CouponWriteOff::getActivityId, id)));
        return activityInfoResDTO;
    }

    /**
     * 撤销活动
     *
     * @param id
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void revoke(Long id) {
        //对于待生效和进行中的活动如果需要进行终止,可以执行撤销活动
        //执行操作后此活动将终止,用户已抢到还未使用的优惠券将作废


        //1.根据活动id查询activity表中的状态数据
        Activity activity = this.getById(id);
        if (ObjectUtils.isNull(activity)) {
            throw new ForbiddenOperationException("活动不存在");
        }

        if (activity.getStatus() != NO_DISTRIBUTE.getStatus()
                && activity.getStatus() != DISTRIBUTING.getStatus()) {
            throw new ForbiddenOperationException("当前活动状态不允许撤销");
        }

        //2.将待生效和进行中的活动---->作废状态
        //update activity set status=作废 where id= 活动id and status in(待生效,进行中)

        boolean flag = this.lambdaUpdate()
                .eq(Activity::getId, id)
                .in(Activity::getStatus, NO_DISTRIBUTE.getStatus(), DISTRIBUTING.getStatus())
                .set(Activity::getStatus, VOIDED.getStatus())
                .update();

        //3.将优惠券表中的  未使用状态-->作废状态
        //update coupon set status=作废 where activity_id=活动id and status=未使用

        if (flag) {
            couponService.lambdaUpdate()
                    .eq(Coupon::getActivityId, id)
                    .eq(Coupon::getStatus, CouponStatusEnum.NO_USE.getStatus())
                    .set(Coupon::getStatus, CouponStatusEnum.VOIDED.getStatus())
                    .update();
        }

    }


    /**
     * 定时更新活动状态
     */
    @Override
    public void updateStatus() {
        /*活动状态包括：1 待生效，2 进行中，3 已失效，4 作废*/


        //对于待生效的活动到达发放开始时间状态改为进行中
        //update activity set status=进行中 where status=待生效 and 发放开始时间<=当前时间  发放结束时间>当前时间
        this.lambdaUpdate()
                .eq(Activity::getStatus, NO_DISTRIBUTE.getStatus())
                .le(Activity::getDistributeStartTime, LocalDateTime.now())
                .ge(Activity::getDistributeEndTime, LocalDateTime.now())
                .set(Activity::getStatus, DISTRIBUTING.getStatus())
                .update();


        //对于 待生效 及进行中 的活动到达发放结束时间状态改为已失效
        //update activity set status=已失效 where status in(待生效,进行中) and 发放结束时间>=当前时间
        this.lambdaUpdate()
                .in(Activity::getStatus, NO_DISTRIBUTE.getStatus(), DISTRIBUTING.getStatus())
                .lt(Activity::getDistributeEndTime, LocalDateTime.now())
                .set(Activity::getStatus, LOSE_EFFICACY.getStatus())
                .update();


    }

    /**
     * 活动预热
     */
    @Override
    public void preHeat() {
        //查询状态是待开始或者进行中，并且发放开始时间距离现在不足1个月的活动，按照开始时间升序排列
        //select * from activity where status in (待开始,进行中) and 发放时间< 现在时间+30天 order by 发放时间 asc
        List<Activity> list = this.lambdaQuery()
                .in(Activity::getStatus, DISTRIBUTING.getStatus(), NO_DISTRIBUTE.getStatus())//in (待开始,进行中)
                .lt(Activity::getDistributeStartTime, LocalDateTime.now().plusMonths(1))
                .orderByAsc(Activity::getDistributeStartTime).list();

        if (CollUtils.isEmpty(list)) {
            //防止缓存穿透
            list = new ArrayList<>();
            return;
        }
        //将查询到的数据封装到List<SeizeCouponInfoResDTO>
        List<SeizeCouponInfoResDTO> InfoResDTOlist = list.stream().map(e -> {
            return BeanUtils.copyBean(e, SeizeCouponInfoResDTO.class);
        }).collect(Collectors.toList());

        //将转换为JSON串存入redis
        String jsonStr = JsonUtils.toJsonStr(InfoResDTOlist);
        redisTemplate.opsForValue().set(ACTIVITY_CACHE_LIST, jsonStr);



        //4.这里是数据库同步redis中的操作

        //1).待生效的活动直接更新在redis中
        list.stream().filter(e->
            getStatus(e.getDistributeStartTime(),e.getDistributeEndTime(),e.getStatus())==NO_DISTRIBUTE.getStatus()
        ).forEach(e->{
            //在redis中进行一个更新
            redisTemplate.opsForHash().put(String.format(COUPON_RESOURCE_STOCK,e.getId()%10),e.getId(),e.getStockNum());
        });

        //2).进行中的活动,如果已经更新库存则就不更新库存了
        list.stream().filter(e->
                getStatus(e.getDistributeStartTime(),e.getDistributeEndTime(),e.getStatus())==DISTRIBUTING.getStatus()
        ).forEach(e->{
            //在redis中进行一个更新 putIdAbsent
            redisTemplate.opsForHash().putIfAbsent(String.format(COUPON_RESOURCE_STOCK,e.getId()%10),e.getId(),e.getStockNum());
        });

    }

    @Override
    public List<SeizeCouponInfoResDTO> queryForListFromCache(Integer tabType) {
        //1.从redis中获取到优惠券数据
        String jsonStr = (String) redisTemplate.opsForValue().get(ACTIVITY_CACHE_LIST);

        if (StringUtils.isEmpty(jsonStr)) {
            return List.of();
        }
        //2.筛选出复合条件的数据
        //将字符串转换成集合
        List<SeizeCouponInfoResDTO> list = JsonUtils.toList(jsonStr, SeizeCouponInfoResDTO.class);
        if (CollUtils.isEmpty(list)) {
            return List.of();
        }

        //3.根据tabType的指筛选复合条件的活动返回
        // type = 1 疯抢  =  活动状态=进行中
        // type = 2 即将开始  =  活动状态=待开始

       return list.stream().filter(e -> {
           //由于redis中的状态更新是有延迟,所以为了更准确的判断当前项目的真实状态,所以写了要给方法判断当前对象的真实状态
           int status = getStatus(e.getDistributeStartTime(), e.getDistributeEndTime(), e.getStatus());//这个就是真实状态
           if (tabType == 1)  //就是状态为进行中的
            {
                return status == DISTRIBUTING.getStatus();
            } else {
                return status == NO_DISTRIBUTE.getStatus();
            }
        }).map(e -> {
             e.setRemainNum(e.getStockNum());
             return e;
        }).collect(Collectors.toList());

    }



    @Override
    public ActivityInfoResDTO getActivityInfoByIdFromCache(Long id) {
        //1. 缓存中获取活动信息
        String jsonString = (String) redisTemplate.opsForValue().get(ACTIVITY_CACHE_LIST);
        if (StringUtils.isEmpty(jsonString)) {
            return null;
        }

        //2. 字符串转换为集合
        List<ActivityInfoResDTO> activityInfoResDTOList = JSON.parseArray(jsonString, ActivityInfoResDTO.class);
        if (CollUtil.isEmpty(activityInfoResDTOList)) {
            return null;
        }

        //3. 过滤出指定id的活动
        return activityInfoResDTOList.stream()
                .filter(e -> e.getId().equals(id))
                .findFirst().orElse(null);
    }

    /**
     * 根据活动的目前状态、开始、结束时间 对比当前时间来获取到活动的真实状态
     * 1. 状态在待生效, 但是 活动开始时间 <=当前时间 < 活动结束时间  真实状态应该是 进行中
     * 2. 状态在待生效, 但是 活动结束时间 < 当前时间               真实状态应该是 已结束
     * 3. 状态在进行中, 但是 活动结束时间 < 当前时间               真实状态应该是 已结束
     * 4. 其它情况, 当前状态就是真实状态
     *
     * @param distributeStartTime 活动开始时间
     * @param distributeEndTime   活动结束时间
     * @param status              当前状态
     * @return 活动的真实状态
     */
    private int getStatus(LocalDateTime distributeStartTime, LocalDateTime distributeEndTime, Integer status) {
        if (NO_DISTRIBUTE.getStatus() == status && distributeStartTime.isBefore(DateUtils.now()) && distributeEndTime.isAfter(LocalDateTime.now())) {
            //状态在待生效, 但是 活动开始时间<=当前时间<活动结束时间  真实状态应该是 进行中
            return DISTRIBUTING.getStatus();
        } else if (NO_DISTRIBUTE.getStatus() == status && distributeEndTime.isBefore(LocalDateTime.now())) {
            //状态在待生效, 但是 活动结束时间 < 当前时间   真实状态应该是 已结束
            return LOSE_EFFICACY.getStatus();
        } else if (DISTRIBUTING.getStatus() == status && distributeEndTime.isBefore(LocalDateTime.now())) {
            //状态在进行中, 但是 活动结束时间 < 当前时间   真实状态应该是 已结束
            return LOSE_EFFICACY.getStatus();
        }
        return status;
    }



}
